Bienvenidos a la Actividad 1, donde pondremos en práctica todo lo aprendido durante el bloque 2. Esta actividad la realizaremos en clase, se terminará en casa (debería completarse en clase) y se entregará el día 8 de octubre.
Vamos a poner en práctica cuatro aspectos del procesamiento de imágenes:
La finalidad es sencilla. Se os dará una imagen, a color, que tiene varias tonalidades y que está pintada con círculos.
La actividad consiste en contar el número de círculos de la imagen.
Se evaluará de la siguiente manera:
No se aceptará el formato .ipynb Habilitaré una actividad en Canvas para que podáis subir ambos archivos.
En primer lugar, cargamos todos los paquetes/frameworks que nos van a hacer falta. Se recomienda visitar la web: https://scikit-image.org/ para ver todas las funcionalidades que permite Scikit Image.
# Paquetes necesarios para la realización de esta práctica (no son necesarios conocerlos ni entenderlos por ahora)
from skimage.io import imread
from skimage import io, color
from skimage import transform as tf
import matplotlib.pyplot as plt
# Cargamos la función para convertir de RGB a Escala de grises
from skimage.color import rgb2gray
# Paquete y funciones para realizar una umbralización con Scikit-image
from skimage.filters import threshold_otsu, threshold_local, threshold_niblack, threshold_sauvola
# Paquetes necesarios para la morfología matemática
from skimage.morphology import erosion, dilation, opening, closing
# Elementos estructurales
from skimage.morphology import disk, diamond, ball, rectangle
# Estas dos funciones nos sirven para detectar los objetos dentro de una imagen binaria
from skimage.morphology import label
from skimage.measure import regionprops
# Defino una función para mostrar una imagen por pantalla con el criterio que considero más acertado
def imshow(img, title):
fig, ax = plt.subplots(figsize=(7, 7))
# El comando que realmente muestra la imagen
ax.imshow(img,cmap=plt.cm.gray)
# Para evitar que aparezcan los números en los ejes
ax.set_xticks([]), ax.set_yticks([])
ax.set_title(title)
plt.show()
Lo primero de todo, vamos a leer la imagen. Recuerda que hay que subir la imagen cada vez que inicies sesión en el notebook y que la ruta se mira haciendo botón derecho sobre el archivo.
Con lo cual, aquí vamos a hacer dos cosas:
Hacemos esto para luego posteriormente umbralizar la imagen en escala de grises.
import matplotlib.image as mpimg #Accedo a las funciones relacionadas con imágenes proporcionada por la biblioteca matplotlib
# Cargo la imagen utilizando imread
IMG = mpimg.imread("Pintura_Puntos.jpg")
# Muestro la imagen con imshow
plt.imshow(IMG)
<matplotlib.image.AxesImage at 0x7f41b836ae30>
# Creo la misma imagen pero en escala de grises con color.rgb2gray()
IMG_gris = color.rgb2gray(IMG)
# Muestro la imagen en escala de grises
plt.imshow(IMG_gris, cmap='gray')
# Desactivo los ejes
plt.axis('off')
plt.show()
Vamos a probar ahora diferentes métodos para umbralizar la imagen. Se pide en esta actividad:
* threshold_otsu, threshold_local, threshold_niblack, threshold_sauvola
UMBRALIZACION GLOBAL
Uglobal = threshold_otsu(IMG_gris)
# Aplicar la umbralización
ImagenUmb = IMG_gris > Uglobal
# Mostrar la imagen umbralizada
plt.figure(figsize=(10, 5))
plt.subplot(121)
plt.imshow(IMG_gris, cmap='gray')
plt.axis('off')
plt.title('Imagen Original')
plt.subplot(122)
plt.imshow(ImagenUmb, cmap='gray')
plt.title('Imagen Umbralizada')
plt.axis('off')
plt.show()
Esta función calcula el umbral óptimo para una imagen en escala de grises de modo que la varianza intraclase (varianza dentro de las dos clases, blanco y negro) sea mínima. Busca el umbral que maximiza la separación entre los objetos y el fondo en la imagen. Es útil cuando el contraste entre objetos y fondo es alto.
UMBRALIZACIÓN LOCAL
# Calculo la umbralización local utilizando threshold_local
block_size = 55 # Tamaño del bloque
Ulocal = threshold_local(IMG_gris, block_size)
# Aplico la umbralización local
ImgULocal = IMG_gris > Ulocal
# Mostrar la imagen original y la imagen umbralizada
plt.figure(figsize=(10, 5))
plt.subplot(121)
plt.imshow(IMG_gris, cmap='gray')
plt.axis('off')
plt.title('Imagen Original')
plt.subplot(122)
plt.imshow(ImgULocal, cmap='gray')
plt.title('Imagen Umbralizada (Local)')
plt.axis('off')
plt.show()
Esta función calcula umbrales locales para cada región de una imagen en función de un tamaño de bloque especificado. Es útil para imágenes con iluminación variable o no uniforme. Se puede ajustar el tamaño de la ventana para adaptarlo a las características locales de la imagen.
block_size determina el tamaño del bloque de vecindario para el cálculo del umbral local. He elegido 55.
UMBRALIZACIÓN NIBLACK
Se trata de un tipo de umbralización local, por lo que también utiliza una ventana, pero utiliza un método diferente que considera el promedio y la desviación estándar local para calcular el umbral.
block_size = 65 # Tamaño del bloque para threshold_niblack
umbral_niblack = threshold_niblack(IMG_gris, window_size=block_size)
# Aplico la umbralización local (threshold_niblack)
imagen_umbralizada_niblack = IMG_gris > umbral_niblack
# Muestro la imagen original y la imagen umbralizada (threshold_niblack)
plt.figure(figsize=(10, 5))
plt.subplot(121)
plt.imshow(IMG_gris, cmap='gray')
plt.axis('off')
plt.title('Imagen Original')
plt.subplot(122)
plt.imshow(imagen_umbralizada_niblack, cmap='gray')
plt.title('Imagen Umbralizada (threshold_niblack)')
plt.axis('off')
plt.show()
Utilizo window_size, que representa el tamaño de la ventana de vecindario que se utilizará para calcular el umbral local. block_size se utiliza como el valor de window_size. La ventana de vecindario se desliza por toda la imagen y en cada posición se calcula un umbral local basado en los píxeles dentro de la ventana.
UMBRALIZACIÓN SAUVOLA
# Calculo la umbralización local utilizando threshold_sauvola
window_size = 51 # Tamaño de la ventana para threshold_sauvola
umbral_sauvola = threshold_sauvola(IMG_gris, window_size=window_size)
# Aplicar la umbralización local (threshold_sauvola)
imagen_umbralizada_sauvola = IMG_gris > umbral_sauvola
# Mostrar la imagen original y la imagen umbralizada (threshold_sauvola)
plt.figure(figsize=(10, 5))
plt.subplot(121)
plt.imshow(IMG_gris, cmap='gray')
plt.axis('off')
plt.title('Imagen Original')
plt.subplot(122)
plt.imshow(imagen_umbralizada_sauvola, cmap='gray')
plt.title('Imagen Umbralizada (threshold_sauvola)')
plt.axis('off')
plt.show()
Esta función calcula umbrales locales utilizando un método basado en la media y la desviación estándar local, similar a threshold_niblack. Sin embargo, threshold_sauvola considera una ponderación adicional que tiene en cuenta las diferencias de contraste en regiones de diferentes tamaños.
Conclusión: Podemos ver que, de todas las umbralizaciones, obtenemos el mejor resultado de la umbralización local. Vemos como tiene una segmentación más precisa al ajustar el umbral en regiones específicas, como mantiene mejor los detalles y como separa los objetos conectados, a diferencia del resto.
import numpy as np
# Rotar la imagen 180 grados
imagen_rotada = np.rot90(np.rot90(IMG_gris))
# Mostrar la imagen rotada
plt.figure(figsize=(10, 5))
plt.subplot(121)
plt.imshow(imagen_rotada, cmap='gray')
plt.axis('off')
plt.title('Imagen 180 grados')
plt.show()
# Calculo la umbralización local utilizando threshold_local
block_size = 55 # Tamaño del bloque
Ulocal = threshold_local(imagen_rotada, block_size)
# Aplico la umbralización local
ImgURLocal = imagen_rotada > Ulocal
# Mostrar la imagen original y la imagen umbralizada
plt.figure(figsize=(10, 5))
plt.subplot(121)
plt.imshow(imagen_rotada, cmap='gray')
plt.axis('off')
plt.title('Imagen Original Rotada')
plt.subplot(122)
plt.imshow(ImgURLocal, cmap='gray')
plt.title('Imagen Umbralizada Rotada(Local)')
plt.axis('off')
plt.show()
¿Se obtiene el mismo resultado si se rota la imagen 180º?¿Por qué?
No, generalmente no se obtiene el mismo resultado si se umbraliza una imagen con umbral local antes y después de rotarla 180 grados. Esto se debe a que la rotación de la imagen cambia la distribución espacial de los píxeles y, por lo tanto, afecta a la umbralización local.
La umbralización local se basa en calcular umbrales adaptativos para regiones o ventanas pequeñas de la imagen. Estos umbrales dependen de la distribución de intensidades locales dentro de esas ventanas. Cuando rotas la imagen 180 grados, la relación espacial y las orientaciones de las características en la imagen cambian drásticamente. Como resultado, las regiones que se consideran como objetos y fondo pueden cambiar, y las características locales que se utilizan para calcular los umbrales también se alterarán.
# Este es el mejor resultado que tendríais que alcanzar
Como se puede apreciar en la imagen hay varios elementos imperfectos:
Mediante el uso de morfología matemática (concretamente los cuatro operadores visto en clase) y los posibles elementos estructurales existentes, se pide:
EROSIÓN
#Utilizaremos la imagen umbralizada loca: ImgULocal
# Defino el elemento estructural
elemento_estructural_disco = disk(5) # Elemento estructural tipo disco
elemento_estructural_rectangulo = rectangle(5, 5) # Elemento estructural tipo rectángulo
# Aplico la erosión con el elemento estructural tipo disco
imagen_erosionada_disco = erosion(ImgULocal, elemento_estructural_disco)
# Aplicar la erosión con el elemento estructural tipo rectángulo
imagen_erosionada_rectangulo = erosion(ImgULocal, elemento_estructural_rectangulo)
# Mostrar las imágenes originales y erosionadas
plt.figure(figsize=(12, 4))
plt.subplot(131)
plt.imshow(ImgULocal, cmap='gray')
plt.title('Imagen Umbralizada Local')
plt.axis('off')
plt.subplot(132)
plt.imshow(imagen_erosionada_disco, cmap='gray')
plt.title('Erosión (Disco)')
plt.axis('off')
plt.subplot(133)
plt.imshow(imagen_erosionada_rectangulo, cmap='gray')
plt.title('Erosión (Rectángulo)')
plt.axis('off')
plt.show()
Inicialmente utilicé ambos elementos estructurales con un tamaño de 5 (disco de radio 5 y rectangulo 5x5). Sin embargo, como el tamaño del elemento estructural debe estar relacionado con el tamaño de los objetos que deseo analizar en la imagen, decidí reducir el tamaño a 1.
Si los objetos son pequeños, el elemento estructural debe ser pequeño, y si son grandes, el elemento debe ser más grande.
# Defino el elemento estructural
elemento_estructural_disco = disk(1) # Elemento estructural tipo disco
elemento_estructural_rectangulo = rectangle(1, 1) # Elemento estructural tipo rectángulo 1x1
# Aplico la erosión con el elemento estructural tipo disco
imagen_erosionada_disco = erosion(ImgULocal, elemento_estructural_disco)
# Aplico la erosión con el elemento estructural tipo rectángulo
imagen_erosionada_rectangulo = erosion(ImgULocal, elemento_estructural_rectangulo)
# Muestro las imágenes originales y erosionadas
plt.figure(figsize=(12, 4))
plt.subplot(131)
plt.imshow(ImgULocal, cmap='gray')
plt.title('Imagen Umbralizada Local')
plt.axis('off')
plt.subplot(132)
plt.imshow(imagen_erosionada_disco, cmap='gray')
plt.title('Erosión (Disco)')
plt.axis('off')
plt.subplot(133)
plt.imshow(imagen_erosionada_rectangulo, cmap='gray')
plt.title('Erosión (Rectángulo)')
plt.axis('off')
plt.show()
Como la imagen tiene círculos pequeños, la erosión con un elemento de disco parece ser más apropiada porque se adapta bien a la forma circular de los objetos y permite una reducción uniforme del tamaño mientras conserva la forma.
DILATACIÓN
# Defino el elemento estructural
elemento_estructural_disco = disk(5) # Elemento estructural tipo disco
elemento_estructural_rectangulo = rectangle(5, 5) # Elemento estructural tipo rectángulo
# Aplico la dilatación con el elemento estructural tipo disco
imagen_dilatada_disco = dilation(ImgULocal, elemento_estructural_disco)
# Aplico la dilatación con el elemento estructural tipo rectángulo
imagen_dilatada_rectangulo = dilation(ImgULocal, elemento_estructural_rectangulo)
# Muestro las imágenes originales y dilatadas
plt.figure(figsize=(12, 4))
plt.subplot(131)
plt.imshow(ImgULocal, cmap='gray')
plt.title('Imagen Umbralizada Local')
plt.axis('off')
plt.subplot(132)
plt.imshow(imagen_dilatada_disco, cmap='gray')
plt.title('Dilatación (Disco)')
plt.axis('off')
plt.subplot(133)
plt.imshow(imagen_dilatada_rectangulo, cmap='gray')
plt.title('Dilatación (Rectángulo)')
plt.axis('off')
plt.show()
No he obtenido el resultado deseado, por lo que voy a reducir las dimenciones de los elementos estructurales a 2.
# Defino el elemento estructural
elemento_estructural_disco = disk(2) # Elemento estructural tipo disco
elemento_estructural_rectangulo = rectangle(2, 2) # Elemento estructural tipo rectángulo
# Aplico la dilatación con el elemento estructural tipo disco
imagen_dilatada_disco = dilation(ImgULocal, elemento_estructural_disco)
# Aplico la dilatación con el elemento estructural tipo rectángulo
imagen_dilatada_rectangulo = dilation(ImgULocal, elemento_estructural_rectangulo)
# Muestro las imágenes originales y dilatadas
plt.figure(figsize=(12, 4))
plt.subplot(131)
plt.imshow(ImgULocal, cmap='gray')
plt.title('Imagen Umbralizada Local')
plt.axis('off')
plt.subplot(132)
plt.imshow(imagen_dilatada_disco, cmap='gray')
plt.title('Dilatación (Disco)')
plt.axis('off')
plt.subplot(133)
plt.imshow(imagen_dilatada_rectangulo, cmap='gray')
plt.title('Dilatación (Rectángulo)')
plt.axis('off')
plt.show()
En este caso, obtenemos mejores resultados utilizando el rectangulo como elemento estructural.
La dilatación con un elemento de rectángulo, en este caso, expande y modifica los objetos de acuerdo con la forma y el tamaño del rectángulo. Esto nos ayuda a mejorar la conectividad o el alargamiento de objetos en una dirección específica.
APERTURA
# Defino el elemento estructural
elemento_estructural_disco = disk(5) # Elemento estructural tipo disco
elemento_estructural_rectangulo = rectangle(5, 5) # Elemento estructural tipo rectángulo
# Aplico la apertura con el elemento estructural tipo disco
imagen_apertura_disco = opening(ImgULocal, elemento_estructural_disco)
# Aplico la apertura con el elemento estructural tipo rectángulo
imagen_apertura_rectangulo = opening(ImgULocal, elemento_estructural_rectangulo)
# Muestro las imágenes originales y después de la apertura
plt.figure(figsize=(12, 4))
plt.subplot(131)
plt.imshow(ImgULocal, cmap='gray')
plt.title('Imagen Umbralizada Local')
plt.axis('off')
plt.subplot(132)
plt.imshow(imagen_apertura_disco, cmap='gray')
plt.title('Apertura (Disco)')
plt.axis('off')
plt.subplot(133)
plt.imshow(imagen_apertura_rectangulo, cmap='gray')
plt.title('Apertura (Rectángulo)')
plt.axis('off')
plt.show()
Reduzco de nuevo las dimensiones de los elementos estructurales
# Defino el elemento estructural
elemento_estructural_disco = disk(2) # Elemento estructural tipo disco
elemento_estructural_rectangulo = rectangle(2, 2) # Elemento estructural tipo rectángulo
# Aplico la apertura con el elemento estructural tipo disco
imagen_apertura_disco = opening(ImgULocal, elemento_estructural_disco)
# Aplico la apertura con el elemento estructural tipo rectángulo
imagen_apertura_rectangulo = opening(ImgULocal, elemento_estructural_rectangulo)
# Muestro las imágenes originales y después de la apertura
plt.figure(figsize=(12, 4))
plt.subplot(131)
plt.imshow(ImgULocal, cmap='gray')
plt.title('Imagen Umbralizada Local')
plt.axis('off')
plt.subplot(132)
plt.imshow(imagen_apertura_disco, cmap='gray')
plt.title('Apertura (Disco)')
plt.axis('off')
plt.subplot(133)
plt.imshow(imagen_apertura_rectangulo, cmap='gray')
plt.title('Apertura (Rectángulo)')
plt.axis('off')
plt.show()
La apertura es una operación que consiste en aplicar primero una erosión a una imagen y luego una dilatación a la imagen erosionada. Esta operación se utiliza para eliminar pequeños objetos o detalles ruidosos en una imagen mientras se preservan los objetos más grandes y estructuras de interés.
En cuanto a la elección del elemento estructural para la apertura, me quedo con el tipo disco, ya que es capaz de preservar la forma circular de los círculos mientras elimina objetos o detalles más pequeños.
CLAUSURA
# Defino el elemento estructural
elemento_estructural_disco = disk(5) # Elemento estructural tipo disco
elemento_estructural_rectangulo = rectangle(5, 5) # Elemento estructural tipo rectángulo
# Aplico la clausura con el elemento estructural tipo disco
imagen_clausura_disco = closing(ImgULocal, elemento_estructural_disco)
# Aplico la clausura con el elemento estructural tipo rectángulo
imagen_clausura_rectangulo = closing(ImgULocal, elemento_estructural_rectangulo)
# Muestro las imágenes originales y después de la clausura
plt.figure(figsize=(12, 4))
plt.subplot(131)
plt.imshow(ImgULocal, cmap='gray')
plt.title('Imagen UmbralizadaLocal')
plt.axis('off')
plt.subplot(132)
plt.imshow(imagen_clausura_disco, cmap='gray')
plt.title('Clausura (Disco)')
plt.axis('off')
plt.subplot(133)
plt.imshow(imagen_clausura_rectangulo, cmap='gray')
plt.title('Clausura (Rectángulo)')
plt.axis('off')
plt.show()
# Defino el elemento estructural
elemento_estructural_disco = disk(3) # Elemento estructural tipo disco
elemento_estructural_rectangulo = rectangle(3, 3) # Elemento estructural tipo rectángulo
# Aplico la clausura con el elemento estructural tipo disco
imagen_clausura_disco = closing(ImgULocal, elemento_estructural_disco)
# Aplico la clausura con el elemento estructural tipo rectángulo
imagen_clausura_rectangulo = closing(ImgULocal, elemento_estructural_rectangulo)
# Muestro las imágenes originales y después de la clausura
plt.figure(figsize=(12, 4))
plt.subplot(131)
plt.imshow(ImgULocal, cmap='gray')
plt.title('Imagen UmbralizadaLocal')
plt.axis('off')
plt.subplot(132)
plt.imshow(imagen_clausura_disco, cmap='gray')
plt.title('Clausura (Disco)')
plt.axis('off')
plt.subplot(133)
plt.imshow(imagen_clausura_rectangulo, cmap='gray')
plt.title('Clausura (Rectángulo)')
plt.axis('off')
plt.show()
La clausura cierra pequeños huecos y une objetos en una imagen al aplicar primero una dilatación seguida de una erosión. El elemento estructural de rectángulo considero que ha sido más efectivo en la clausura porque se suele utilizar cuando se desean unir objetos en direcciones específicas y cerrar espacios alargados debido a su forma rectangular y capacidad para conservar la orientación.
plt.subplot(121)
plt.imshow(imagen_erosionada_disco, cmap='gray')
plt.title('Erosión (Disco)')
plt.axis('off')
plt.subplot(122)
plt.imshow(imagen_dilatada_rectangulo, cmap='gray')
plt.title('Dilatación (Rectángulo)')
plt.axis('off')
plt.show()
plt.subplot(121)
plt.imshow(imagen_apertura_disco, cmap='gray')
plt.title('Apertura (Disco)')
plt.axis('off')
plt.subplot(122)
plt.imshow(imagen_clausura_rectangulo, cmap='gray')
plt.title('Clausura (Rectángulo)')
plt.axis('off')
(-0.5, 1919.5, 1929.5, -0.5)
CONCLUSIÓN: El mejor operador para mí es la APERTURA (erosión + dilatación). Es la mejor opción porque elimina el ruido y detalles no deseados mientras preserva la forma y el tamaño de los círculos en la imagen, mejorando la calidad y la claridad de los objetos.
IMAGEN COMPLEMENTARIA CON APERTURA (DISCO)
La imagen complementaria es una imagen en la que los valores de píxeles se invierten: los píxeles oscuros se vuelven claros y viceversa.
from skimage import io, util
imagen_complementaria = util.invert(ImgULocal)
# Defio el elemento estructural de disco
elemento_estructural_disco = disk(2) # Elemento estructural tipo disco
# Aplico la apertura con el elemento de disco a la imagen complementaria
imagen_resultante = opening(imagen_complementaria, elemento_estructural_disco)
# Muestro la imagen complementaria y la imagen resultante
plt.figure(figsize=(12, 4))
plt.subplot(131)
plt.imshow(ImgULocal, cmap='gray')
plt.title('Imagen UmbralizadaLocal')
plt.axis('off')
plt.subplot(132)
plt.imshow(imagen_complementaria, cmap='gray')
plt.title('Imagen Complementaria')
plt.axis('off')
plt.subplot(133)
plt.imshow(imagen_resultante, cmap='gray')
plt.title('Resultado de Apertura con Disco')
plt.axis('off')
plt.show()
Haciendo uso de las funcionalidades cargadas al principio, se pide hacer una función que:
Por último, ¿qué se podría hacer para asegurar que no se tienen en cuenta posibles errores en la umbralización como pequeños puntos o posible ruido que haya llegado hasta este punto?
import numpy as np
def contar_circulos(imagen_binaria):
# Comprobar que la imagen es binaria
if not np.all(np.logical_or(imagen_binaria == 0, imagen_binaria == 1)):
print("La imagen no es binaria.")
return None
# Etiquetar los objetos en la imagen
etiquetas = label(imagen_binaria)
# Calcular el número de círculos
numero_de_circulos = 0
# Analizar las propiedades de los objetos etiquetados
for region in regionprops(etiquetas):
# Verificar si el objeto es lo suficientemente circular
if region.solidity > 0.8:
numero_de_circulos += 1
return numero_de_circulos
numero_de_circulos = contar_circulos(imagen_apertura_disco)
if numero_de_circulos is not None:
print(f"Número de círculos en la imagen con apertura: {numero_de_circulos}")
else:
print("No se pudo contar los círculos debido a que la imagen no es binaria o hubo un problema en el procesamiento.")
Número de círculos en la imagen con apertura: 5172
He elegido la imagen imagen_apertura_disco para contar los círculos. Para ver la diferencia, también contaré los circulos de la imagen imagen_dilatada_rectangulo.
def contar_circulos(imagen_binaria):
# Comprobar que la imagen es binaria
if not np.all(np.logical_or(imagen_binaria == 0, imagen_binaria == 1)):
print("La imagen no es binaria.")
return None
# Etiquetar los objetos en la imagen
etiquetas = label(imagen_binaria)
# Calcular el número de círculos
numero_de_circulos = 0
# Analizar las propiedades de los objetos etiquetados
for region in regionprops(etiquetas):
# Verificar si el objeto es lo suficientemente circular
if region.solidity > 0.8:
numero_de_circulos += 1
return numero_de_circulos
numero_de_circulos = contar_circulos(imagen_dilatada_rectangulo)
if numero_de_circulos is not None:
print(f"Número de círculos en la imagen dilatada: {numero_de_circulos}")
else:
print("No se pudo contar los círculos debido a que la imagen no es binaria o hubo un problema en el procesamiento.")
Número de círculos en la imagen dilatada: 3976
Vemos como se distinguen mas circulos en la imagen con apertura que en la de dilatación solo.
¿Qué se podría hacer para asegurar que no se tienen en cuenta posibles errores en la umbralización como pequeños puntos o posible ruido que haya llegado hasta este punto?
Filtrado de Área: se podría eliminar objetos en la imagen binaria que tienen un área por debajo de un umbral específico. Los objetos pequeños, como puntos o ruido, generalmente tienen áreas pequeñas.
Filtrado por Aspect Ratio: Si los círculos son los objetos de interés y se espera que tengan una forma circular, se podría calcular el aspect ratio (relación entre ancho y alto) de los objetos etiquetados y eliminar aquellos cuyo aspect ratio esté fuera de un rango específico. Esto ayudará a eliminar objetos alargados que no son círculos.
Filtrado Morfológico: Después de aplicar operaciones morfológicas, como la apertura o clausura, se podría aplicar una operación de eliminación de objetos pequeños utilizando una operación de eliminación de objetos conectados para eliminar regiones pequeñas que puedan representar errores.